Skip to main content

Complete JavaScript Hoisting Guide

Table of Contentsโ€‹

  1. What Hoisting Actually Is
  2. The Two-Phase Mechanism
  3. Hoisting Rules Reference Table
  4. Temporal Dead Zone (TDZ)
  5. var Hoisting
  6. let and const Hoisting
  7. Function Declaration Hoisting
  8. Function Expression Hoisting
  9. Arrow Function Hoisting
  10. Class Hoisting
  11. Interview Problems (Beginner to Advanced)
  12. Hoisting with Closures
  13. Hoisting with Async Code
  14. Block Scope Edge Cases
  15. Advanced Collision Scenarios
  16. typeof Operator Behavior
  17. Common Production Bugs
  18. Mental Model for Interviews
  19. Interview Checklist
  20. Ultra-Hard Puzzles

What Hoisting Actually Isโ€‹

Hoisting โ‰  moving code to the top

This is the most common misconception. JavaScript doesn't physically move your code. Instead, it processes your code in two distinct phases:

The Truth About Hoistingโ€‹

During the creation phase, the JavaScript engine scans through your code and:

  • Registers all variable and function declarations
  • Allocates memory for them
  • Initializes them according to specific rules (depends on declaration type)

During the execution phase, the code runs line by line with those pre-registered bindings already in place.


The Two-Phase Mechanismโ€‹

Phase 1: Creation (Memory Allocation)โ€‹

// What you write:
console.log(x);
var x = 5;

// How JS processes it:
// CREATION PHASE:
var x = undefined; // Memory allocated, initialized to undefined

// EXECUTION PHASE:
console.log(x); // undefined
x = 5; // Assignment happens

Creation phase behavior by type:

Declaration TypeCreatedInitializedValue
varโœ…โœ…undefined
letโœ…โŒuninitialized (TDZ)
constโœ…โŒuninitialized (TDZ)
Function declarationโœ…โœ…Entire function
Function expressionVariable onlyโŒundefined (if var)
Arrow functionVariable onlyโŒundefined (if var)
Classโœ…โŒuninitialized (TDZ)

Phase 2: Executionโ€‹

Code runs line by line, assignments happen, functions are called.


Hoisting Rules Reference Tableโ€‹

ConstructHoistedInitializedAccess before declarationScope
varโœ…undefinedโœ… (returns undefined)Function
letโœ…โŒโŒ (ReferenceError - TDZ)Block
constโœ…โŒโŒ (ReferenceError - TDZ)Block
Function declarationโœ…โœ…โœ… (fully usable)Function/Block*
Function expressionVariable onlyโŒโŒDepends on var/let/const
Arrow functionVariable onlyโŒโŒDepends on var/let/const
Classโœ…โŒโŒ (ReferenceError - TDZ)Block

*Function declarations in blocks are complex and mode-dependent


Temporal Dead Zone (TDZ)โ€‹

The TDZ is the time between entering a scope and the actual declaration line where a variable exists but cannot be accessed.

Basic TDZ Exampleโ€‹

console.log(a); // โŒ ReferenceError: Cannot access 'a' before initialization
let a = 10;

The variable a exists in memory but is in the TDZ until line 2 executes.

TDZ in Block Scopeโ€‹

{
// TDZ starts here for 'x'
console.log(x); // โŒ ReferenceError
let x = 1; // TDZ ends here
}

TDZ with Functionsโ€‹

function test() {
// TDZ starts for 'data'
console.log(data); // โŒ ReferenceError
let data = 100; // TDZ ends
}
test();

TDZ is Temporal, Not Spatialโ€‹

function useX() {
console.log(x); // โŒ ReferenceError
}

let x = 10;
useX(); // This call itself is fine, but the function body accesses x before declaration

Wait, this actually works because by the time useX() is called, x is already declared. The TDZ only exists during the execution of the scope where the variable is declared.

Correct example:

let x = 10;

function useX() {
console.log(x); // โœ… 10 - works fine
}

useX();

TDZ matters when accessing before declaration in the same scope:

function useX() {
console.log(x); // โŒ ReferenceError - TDZ
let x = 10; // Declaration in same scope
}

useX();

typeof in TDZโ€‹

console.log(typeof a); // โŒ ReferenceError
let a = 10;

But with undeclared variables:

console.log(typeof undeclaredVariable); // โœ… "undefined"

๐Ÿ‘‰ Key insight: typeof does NOT bypass TDZ for declared variables.


var Hoistingโ€‹

var declarations are hoisted and initialized to undefined.

Basic var Hoistingโ€‹

console.log(x); // undefined
var x = 5;
console.log(x); // 5

Behind the scenes:

var x = undefined;  // Creation phase
console.log(x); // undefined
x = 5; // Execution phase
console.log(x); // 5

var in Functionsโ€‹

function test() {
console.log(a); // undefined
var a = 10;
console.log(a); // 10
}
test();

var Scope (Function-scoped)โ€‹

function example() {
if (true) {
var x = 10;
}
console.log(x); // โœ… 10 - var ignores block scope
}
example();

Multiple var Declarationsโ€‹

var a = 1;
var a = 2;
console.log(a); // 2 - allowed, no error

var in Global Scopeโ€‹

var globalVar = 'test';
console.log(window.globalVar); // 'test' (in browsers)

let and const Hoistingโ€‹

Both let and const are hoisted but not initialized, creating a TDZ.

let Hoistingโ€‹

console.log(x); // โŒ ReferenceError
let x = 10;

const Hoistingโ€‹

console.log(y); // โŒ ReferenceError
const y = 20;

Block Scopeโ€‹

{
let x = 10;
const y = 20;
}
console.log(x); // โŒ ReferenceError - not defined outside block
console.log(y); // โŒ ReferenceError

No Re-declarationโ€‹

let a = 1;
let a = 2; // โŒ SyntaxError: Identifier 'a' has already been declared
const b = 1;
const b = 2; // โŒ SyntaxError

const Requires Initializationโ€‹

const x; // โŒ SyntaxError: Missing initializer in const declaration

Function Declaration Hoistingโ€‹

Function declarations are fully hoisted and initialized.

Basic Function Hoistingโ€‹

sayHello(); // โœ… "Hello!"

function sayHello() {
console.log("Hello!");
}

Behind the scenes:

// Creation phase:
function sayHello() {
console.log("Hello!");
}

// Execution phase:
sayHello(); // โœ… "Hello!"

Function Overridingโ€‹

foo(); // "second"

function foo() {
console.log("first");
}

function foo() {
console.log("second");
}

Last declaration wins during creation phase.


Function Expression Hoistingโ€‹

Function expressions only hoist the variable, not the function.

var Function Expressionโ€‹

sayHi(); // โŒ TypeError: sayHi is not a function

var sayHi = function() {
console.log("Hi!");
};

Why TypeError, not ReferenceError?

// Behind the scenes:
var sayHi = undefined; // Hoisted
sayHi(); // undefined is not a function
sayHi = function() { // Assignment happens after
console.log("Hi!");
};

let/const Function Expressionโ€‹

greet(); // โŒ ReferenceError: Cannot access 'greet' before initialization

const greet = function() {
console.log("Greetings!");
};

Arrow Function Hoistingโ€‹

Arrow functions behave like function expressions.

const Arrow Functionโ€‹

sayHi(); // โŒ ReferenceError

const sayHi = () => {
console.log("Hi!");
};

var Arrow Functionโ€‹

sayHi(); // โŒ TypeError: sayHi is not a function

var sayHi = () => {
console.log("Hi!");
};

Class Hoistingโ€‹

Classes are hoisted but remain in TDZ until declaration.

Basic Class Hoistingโ€‹

const obj = new Person(); // โŒ ReferenceError

class Person {
constructor(name) {
this.name = name;
}
}

Class Expressionโ€‹

const obj = new MyClass(); // โŒ ReferenceError

const MyClass = class {
constructor() {}
};

Interview Problemsโ€‹

๐Ÿ”ฅ Problem 1: var in Function Scopeโ€‹

function test() {
console.log(a);
var a = 10;
}
test();

Output: undefined

Explanation: var a is hoisted to the top of the function and initialized to undefined.


๐Ÿ”ฅ Problem 2: let vs varโ€‹

console.log(a);
var a = 1;

console.log(b);
let b = 2;

Output:

undefined
ReferenceError: Cannot access 'b' before initialization

๐Ÿ”ฅ Problem 3: Function Declaration vs varโ€‹

foo();

function foo() {
console.log("function");
}

var foo = function() {
console.log("var");
};

Output: "function"

Explanation:

// Creation phase:
function foo() { console.log("function"); } // Function fully hoisted
var foo = undefined; // var is also hoisted but doesn't override function

// Execution phase:
foo(); // Calls the function
var foo = function() { console.log("var"); }; // Reassignment happens after

๐Ÿ”ฅ Problem 4: Function in Block (Strict vs Non-Strict)โ€‹

"use strict";
{
function foo() {
return "inside block";
}
}
foo(); // โŒ ReferenceError in strict mode

Non-strict mode (browser-dependent):

{
function foo() {
return "inside block";
}
}
foo(); // May work in some browsers

๐Ÿ‘‰ Best practice: Never use function declarations inside blocks. Use function expressions instead.


๐Ÿ”ฅ Problem 5: Arrow Function Hoistingโ€‹

sayHi();

const sayHi = () => {
console.log("hi");
};

Output: ReferenceError: Cannot access 'sayHi' before initialization


๐Ÿ”ฅ Problem 6: Class Hoistingโ€‹

const obj = new Person();

class Person {
constructor(name) {
this.name = name;
}
}

Output: ReferenceError: Cannot access 'Person' before initialization


๐Ÿ”ฅ Problem 7: Shadowingโ€‹

var a = 1;

function test() {
console.log(a);
var a = 2;
}

test();

Output: undefined

Explanation: Local var a shadows the outer a and is hoisted to the top of the function.


๐Ÿ”ฅ Problem 8: Function Parametersโ€‹

function foo(a) {
console.log(a);
var a = 20;
}

foo(10);

Output: 10

Explanation: Parameter a = 10 is already initialized. The var a = 20 declaration is ignored (parameter takes precedence), but the assignment a = 20 would happen after the console.log.


๐Ÿ”ฅ Problem 9: Duplicate Declarationsโ€‹

var a = 1;
var a = 2;
console.log(a); // 2 - allowed

let b = 1;
let b = 2; // โŒ SyntaxError: Identifier 'b' has already been declared

๐Ÿ”ฅ Problem 10: Loop Hoistingโ€‹

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}

Output:

3
3
3

Explanation: var i is function-scoped (or global if not in a function), so there's only one binding shared across all iterations.

Fix with let:

for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 2

let creates a new binding for each iteration.


Hoisting with Closuresโ€‹

Problem 11: var in Closureโ€‹

function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result.push(function() {
return i;
});
}
return result;
}

const funcs = createFunctions();
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3

Explanation: All closures reference the same i variable.

Fix:

function createFunctions() {
var result = [];
for (let i = 0; i < 3; i++) {
result.push(function() {
return i;
});
}
return result;
}

const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2

Problem 12: Closure with setTimeoutโ€‹

for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}

Output: 4, 4, 4 (after 1s, 2s, 3s)

Fix with IIFE:

for (var i = 1; i <= 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, j * 1000);
})(i);
}
// Output: 1, 2, 3

Fix with let:

for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// Output: 1, 2, 3

Hoisting with Async Codeโ€‹

Problem 13: Hoisting in Async Functionsโ€‹

async function test() {
console.log(x); // undefined
var x = 10;
console.log(x); // 10
}
test();

Explanation: Hoisting works the same in async functions.

Problem 14: Await and Hoistingโ€‹

async function getData() {
console.log(data); // โŒ ReferenceError
const data = await fetch('/api');
}

Explanation: const is in TDZ before declaration, even with await.

Problem 15: Promise and varโ€‹

for (var i = 0; i < 3; i++) {
Promise.resolve().then(() => console.log(i));
}

Output: 3, 3, 3

Fix:

for (let i = 0; i < 3; i++) {
Promise.resolve().then(() => console.log(i));
}
// Output: 0, 1, 2

Block Scope Edge Casesโ€‹

Problem 16: Nested Blocksโ€‹

{
console.log(x); // โŒ ReferenceError
{
let x = 10;
}
}

Problem 17: if Blockโ€‹

if (true) {
var x = 10;
}
console.log(x); // โœ… 10
if (true) {
let y = 20;
}
console.log(y); // โŒ ReferenceError

Problem 18: Switch Caseโ€‹

switch (1) {
case 1:
let x = 10;
console.log(x); // 10
break;
case 2:
let x = 20; // โŒ SyntaxError: Identifier 'x' has already been declared
break;
}

Explanation: The entire switch block is one scope.

Fix:

switch (1) {
case 1: {
let x = 10;
console.log(x);
break;
}
case 2: {
let x = 20;
console.log(x);
break;
}
}

Problem 19: for Loop Scopeโ€‹

for (let i = 0; i < 3; i++) {
let i = 'inner'; // Different variable
console.log(i); // 'inner', 'inner', 'inner'
}

Advanced Collision Scenariosโ€‹

Problem 20: Function vs var Collisionโ€‹

console.log(foo); // [Function: foo]

var foo = "variable";
function foo() {
return "function";
}

console.log(foo); // "variable"

Explanation:

// Creation phase:
function foo() { return "function"; } // Function hoisted
var foo; // var declaration (but doesn't override the function)

// Execution phase:
console.log(foo); // [Function: foo]
foo = "variable"; // Assignment happens
console.log(foo); // "variable"

Problem 21: Multiple Functions Same Nameโ€‹

foo(); // "third"

function foo() {
console.log("first");
}

function foo() {
console.log("second");
}

function foo() {
console.log("third");
}

Explanation: Last function declaration wins.

Problem 22: Parameter vs varโ€‹

function test(x) {
console.log(x); // 10
var x;
console.log(x); // 10
x = 20;
console.log(x); // 20
}
test(10);

Explanation: Parameter declaration takes precedence. var x; is redundant.

Problem 23: let in Parameter Defaultโ€‹

function test(a = b, b = 2) {
console.log(a, b);
}
test(); // โŒ ReferenceError: Cannot access 'b' before initialization

Explanation: Parameters are evaluated left to right. b is in TDZ when a's default is evaluated.

Fix:

function test(b = 2, a = b) {
console.log(a, b);
}
test(); // 2, 2

typeof Operator Behaviorโ€‹

Problem 24: typeof with Undeclared Variableโ€‹

console.log(typeof undeclaredVariable); // "undefined"

Problem 25: typeof with TDZโ€‹

console.log(typeof x); // โŒ ReferenceError
let x = 10;

Problem 26: typeof with varโ€‹

console.log(typeof y); // "undefined"
var y = 10;

Common Production Bugsโ€‹

Bug 1: Conditional var Declarationโ€‹

โŒ Buggy:

function checkStatus() {
if (!isReady) {
var isReady = true;
}
return isReady;
}
console.log(checkStatus()); // undefined (not true!)

Behind the scenes:

function checkStatus() {
var isReady; // Hoisted to top, initialized as undefined
if (!isReady) { // undefined is falsy
isReady = true;
}
return isReady;
}

โœ… Fix:

function checkStatus() {
let isReady = false;
if (!isReady) {
isReady = true;
}
return isReady;
}
console.log(checkStatus()); // true

Bug 2: Loop Event Handlersโ€‹

โŒ Buggy:

for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert(i); // Always alerts buttons.length
};
}

โœ… Fix:

for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert(i); // Alerts correct index
};
}

Bug 3: Async var Mutationโ€‹

โŒ Buggy:

for (var i = 0; i < 5; i++) {
fetch('/api/' + i).then(response => {
console.log('Response for', i); // Always 5
});
}

โœ… Fix:

for (let i = 0; i < 5; i++) {
fetch('/api/' + i).then(response => {
console.log('Response for', i); // Correct index
});
}

Mental Model for Interviewsโ€‹

When explaining hoisting in interviews, use this mental model:

"JavaScript processes code in two phases. During the creation phase, it scans through the scope and registers all variable and function declarations, allocating memory and initializing them according to specific rulesโ€”var gets undefined, functions get their full definition, and let/const/classes enter a temporal dead zone where they exist but can't be accessed. Then during the execution phase, the code runs line by line with those pre-registered bindings already in place. This is why you can call a function before it's declared, but accessing a let variable before declaration throws a ReferenceError."

Key phrases to use:

  • "Two-phase mechanism: creation and execution"
  • "Memory allocation vs initialization"
  • "Temporal Dead Zone for let/const"
  • "Scope registration before execution"
  • "Function declarations are fully initialized"

Interview Checklistโ€‹

โœ… Function declarations โ†’ Fully hoisted and initialized โœ… var โ†’ Hoisted, initialized to undefined โœ… let/const/class โ†’ Hoisted but in TDZ, uninitialized โœ… Arrow functions โ†’ Not hoisted as functions โœ… Function expressions โ†’ Variable hoisted (if var), function not โœ… Parameters โ†’ Take precedence over var in same scope โœ… Closures + var โ†’ Single shared binding (common bug) โœ… Block scope โ†’ let/const respect blocks, var doesn't โœ… typeof โ†’ Doesn't bypass TDZ โœ… Switch cases โ†’ Share one block scope


Ultra-Hard Puzzlesโ€‹

๐Ÿ”ฅ Puzzle 1: Complex Shadowingโ€‹

var a = 1;

function outer() {
console.log(a);

function inner() {
console.log(a);
var a = 3;
}

inner();
console.log(a);
var a = 2;
}

outer();
console.log(a);

Output:

undefined
undefined
undefined
1

Explanation:

  • First console.log(a) in outer: local var a is hoisted โ†’ undefined
  • In inner, var a is hoisted โ†’ undefined
  • After inner(), still undefined in outer
  • After outer(), global a is still 1

๐Ÿ”ฅ Puzzle 2: Function Expression Collisionโ€‹

var foo = 1;

function bar() {
console.log(foo);
foo = 10;

function foo() {}

console.log(foo);
}

bar();
console.log(foo);

Output:

[Function: foo]
10
1

Explanation:

  • Function foo is hoisted in bar's scope
  • foo = 10 reassigns the local function
  • Global foo remains 1

๐Ÿ”ฅ Puzzle 3: TDZ with Function Callโ€‹

let x = 1;

function test() {
console.log(x);
let x = 2;
}

test();

Output: ReferenceError: Cannot access 'x' before initialization

Explanation: Local let x creates TDZ in test, shadowing outer x.


๐Ÿ”ฅ Puzzle 4: Multiple Scopesโ€‹

var x = 1;

{
var x = 2;
{
let x = 3;
console.log(x);
}
console.log(x);
}

console.log(x);

Output:

3
2
2

Explanation: var ignores block scope, let respects it.


๐Ÿ”ฅ Puzzle 5: Async + Hoistingโ€‹

for (var i = 0; i < 3; i++) {
setTimeout(() => {
var j = i;
console.log(j);
}, 100);
}

Output: 3, 3, 3 (after 100ms)

Explanation: var i is shared. Each timeout captures the final value of i.


๐Ÿ”ฅ Puzzle 6: Class Expressionโ€‹

const MyClass = class InternalName {
constructor() {
console.log(InternalName);
}
};

new MyClass(); // [class InternalName]
console.log(InternalName); // โŒ ReferenceError

Explanation: Class expression name is only visible inside the class.


๐Ÿ”ฅ Puzzle 7: Destructuring Hoistingโ€‹

console.log(a); // โŒ ReferenceError
let { a } = { a: 1 };
console.log(b); // undefined
var { b } = { b: 2 };

Explanation: Destructuring follows the same hoisting rules as regular declarations.


๐Ÿ”ฅ Puzzle 8: Function Parameter Default with Closureโ€‹

var x = 1;

function foo(x = x) {
console.log(x);
}

foo(); // โŒ ReferenceError: Cannot access 'x' before initialization

Explanation: Parameter x is in TDZ when its own default value is evaluated.


๐Ÿ”ฅ Puzzle 9: Nested Function Hoistingโ€‹

function outer() {
inner(); // โœ… Works

function inner() {
console.log("inner");
}
}

outer();
function outer() {
inner(); // โŒ ReferenceError

const inner = function() {
console.log("inner");
};
}

outer();

๐Ÿ”ฅ Puzzle 10: Mixed Declarationsโ€‹

console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"

var bar = function() {};

function foo() {}

Explanation: Function declaration hoisted completely, function expression only hoists the variable.


๐Ÿ”ฅ Puzzle 11: with Statement (Don't use in production!)โ€‹

var obj = { a: 1 };

with (obj) {
console.log(a); // 1
var a = 2; // Creates global variable!
}

console.log(a); // 2
console.log(obj.a); // 1

Explanation: var inside with doesn't create a property on obj, it creates a global variable.


๐Ÿ”ฅ Puzzle 12: eval and Hoisting (Avoid eval!)โ€‹

function test() {
eval("var x = 10");
console.log(x); // 10 (in non-strict mode)
}

test();

In strict mode:

"use strict";
function test() {
eval("var x = 10");
console.log(x); // โŒ ReferenceError
}

test();

๐Ÿ”ฅ Puzzle 13: Generator Functionโ€‹

gen(); // โŒ ReferenceError

const gen = function* () {
yield 1;
};
gen(); // โœ… Works

function* gen() {
yield 1;
}

Explanation: Generator expressions follow function expression rules, generator declarations follow function declaration rules.


๐Ÿ”ฅ Puzzle 14: Arrow Function with varโ€‹

test(); // โŒ TypeError: test is not a function

var test = () => console.log("arrow");

๐Ÿ”ฅ Puzzle 15: Import Hoistingโ€‹

// This works:
myFunction();

import { myFunction } from './module.js';

Explanation: Imports are hoisted to the top of the module and initialized before any code runs.